# 데이터베이스 메타데이터로 작업하기


SQLAlchemy Core와 ORM은 파이썬 객체를 데이터베이스의 테이블과 컬럼처럼 사용할 수 있게 하기 위해서 만들어졌습니다. 이러한 것들을 데이터베이스 메타데이터로 사용할 수 있습니다.

메타데이터는 데이터를 기술하는 데이터를 설명합니다. 여기서 메타데이터는 구성된 테이블, 열, 제약 조건 및 기타 객체 정보 등을 말합니다.


# 테이블 객체를 만들고 메타데이터에 담기

관계형 데이터베이스에서는 쿼리를 통해 테이블을 만들지만, SQLAlchemy에서는 Python 객체를 통해 테이블을 만들 수 있습니다.
SQLAlchemy 표현 언어를 시작할려면 사용할려는 데이터베이스 테이블을 Table 객체로 만들어줘야합니다.

>>> from sqlalchemy import MetaData
>>> metadata = MetaData()  # 테이블들의 메타 정보를 담게될 객체입니다.
>>>
>>> from sqlalchemy import Table, Column, Integer, String
>>> user_table = Table(
...     'user_account',  # 데이터베이스에 저장될 table 이름입니다.
...     metadata,
...     Column('id', Integer, primary_key=True),  # 이 테이블에 들어갈 컬럼입니다.
...     Column('name', String(30)),
...     Column('fullname', String),
... )
  • Table 객체를 통해 데이터베이스 테이블을 만들 수 있습니다.
  • Column을 통해 테이블의 컬럼을 구현합니다.
    • 기본적으로 Column(컬럼 이름, 데이터 유형) 와 같이 정의합니다.

Table 인스턴스를 만들고나면 다음처럼 만들어진 컬럼 정보를 알 수 있습니다.

>>> user_table.c.name
Column('name', String(length=30), table=<user_account>)

>>> user_table.c.keys()
['id', 'name', 'fullname']

# 단순 제약 선언하기

우리는 위의 user 테이블을 만드는 코드에서 Column('id', Integer, primary_key=True) 구문을 보았습니다.
이는 id 컬럼을 기본키로 둔다고 선언하는 것입니다.
기본키는 암시적으로 PrimaryKeyConstraint 객체에 구조로 선언되어있습니다. 이는 다음처럼 확인할 수 있습니다.

>>> user_table.primary_key
PrimaryKeyConstraint(Column('id', Integer(), table=<user_account>, primary_key=True, nullable=False))

기본키와 더불어 외래키도 다음처럼 선언할 수 있습니다.

>>> from sqlalchemy import ForeignKey
>>> address_table = Table(
...     "address",
...     metadata,
...     Column('id', Integer, primary_key=True),
...     Column('user_id', ForeignKey('user_account.id'), nullable=False),  # ForeignKey 객체로 외래 키를 선언합니다.
...     Column('email_address', String, nullable=False)
... )
  • ForeignKey('테이블 이름.외래 키') 형태로 외래 키 컬럼을 선언할 수 있습니다.
    • 이 때 Column 객체의 데이터타입을 생략할 수 있습니다. 데이터타입은 외래 키에 해당하는 컬럼을 찾아서 자동으로 추론됩니다.
  • 따로 설명하지 않았지만, nullable=False 파라미터와 값을 넘김으로써 컬럼에 NOT NULL 제약 조건을 선언할 수 있습니다.

# 데이터베이스에 적용하기

지금까지 SQLAlchemy로 데이터베이스 테이블을 선언했습니다. 이제 이렇게 선언한 테이블이 실제 데이터베이스에 생성되도록 해봅시다.
다음처럼 metadata.create_all() 을 실행합니다.

>>> metadata.create_all(engine)

# 위 코드는 `metadata` 인스턴스에 기록된 모든 테이블들을 생성합니다.  
# 결과적으로는 아래 쿼리를 실행하게 됩니다.

BEGIN (implicit)
PRAGMA main.table_...info("user_account")
...
PRAGMA main.table_...info("address")
...
CREATE TABLE user_account (
    id INTEGER NOT NULL,
    name VARCHAR(30),
    fullname VARCHAR,
    PRIMARY KEY (id)
)
...
CREATE TABLE address (
    id INTEGER NOT NULL,
    user_id INTEGER NOT NULL,
    email_address VARCHAR NOT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY(user_id) REFERENCES user_account (id)
)
...
COMMIT

# ORM 방식으로 테이블 메타데이터 정의하기

위의 데이터베이스 구조를 만들고 제약조건을 똑같이 사용하지만, 이번에는 ORM 방식으로 진행해보겠습니다.


# 레지스트리 설정하기

먼저 다음처럼 registry 객체를 만듭니다.

>>> from sqlalchemy.orm import registry
>>> mapper_registry = registry()

registery 객체는 MetaData 객체를 포함하고 있습니다.

>>> mapper_registry.metadata
MetaData()

이제 다음을 실행합니다.

>>> Base = mapper_registry.generate_base()

위 과정을 다음처럼 declarative_base 로 한 번에 할 수 있습니다.

>>> from sqlalchemy.orm import declarative_base
>>> Base = declarative_base()

# ORM 객체 선언하기

Base 객체를 상속받는 하위 객체를 정의함으로써 ORM 방식으로 데이터베이스의 테이블을 선언할 수 있습니다.

>>> from sqlalchemy.orm import relationship
>>> class User(Base):
...     __tablename__ = 'user_account'  # 데이터베이스에서 사용할 테이블 이름입니다.
...
...     id = Column(Integer, primary_key=True)
...     name = Column(String(30))
...     fullname = Column(String)
...
...     addresses = relationship("Address", back_populates="user")
...
...     def __repr__(self):
...        return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

>>> class Address(Base):
...     __tablename__ = 'address'
...
...     id = Column(Integer, primary_key=True)
...     email_address = Column(String, nullable=False)
...     user_id = Column(Integer, ForeignKey('user_account.id'))
...
...     user = relationship("User", back_populates="addresses")
...
...     def __repr__(self):
...         return f"Address(id={self.id!r}, email_address={self.email_address!r})"

User, Address 객체는 Table 객체를 포함합니다.
다음처럼 __table__ 속성을 통해 확인할 수 있습니다.

>>> User.__table__
Table('user_account', MetaData(),
    Column('id', Integer(), table=<user_account>, primary_key=True, nullable=False),
    Column('name', String(length=30), table=<user_account>),
    Column('fullname', String(), table=<user_account>), schema=None)

# ORM 객체 생성하기

위에서 정의한 뒤, 다음처럼 ORM 객체를 생성할 수 있습니다.

>>> sandy = User(name="sandy", fullname="Sandy Cheeks")
>>> sandy
User(id=None, name='sandy', fullname='Sandy Cheeks')

# 데이터베이스에 적용하기

이제 다음처럼 ORM으로 선언한 테이블을 실제로 데이터베이스에 적용되도록 할 수 있습니다.

>>> mapper_registry.metadata.create_all(engine)
>>> Base.metadata.create_all(engine)

# 기존 데이터베이스의 테이블을 ORM 객체로 불러오기

위의 모든 방법들은 테이블을 직접 선언하지않고, 데이터베이스에 테이블을 가져오는 방법이 있습니다.
코드는 아래와 같습니다.

>>> some_table = Table("some_table", metadata, autoload_with=engine)

BEGIN (implicit)
PRAGMA main.table_...info("some_table")
[raw sql] ()
SELECT sql FROM  (SELECT * FROM sqlite_master UNION ALL   SELECT * FROM sqlite_temp_master) WHERE name = ? AND type = 'table'
[raw sql] ('some_table',)
PRAGMA main.foreign_key_list("some_table")
...
PRAGMA main.index_list("some_table")
...
ROLLBACK

이제 다음과 같이 사용할 수 있습니다.

>>> some_table
Table('some_table', MetaData(),
    Column('x', INTEGER(), table=<some_table>),
    Column('y', INTEGER(), table=<some_table>),
    schema=None)